// Copyright (C) 2022 即时通讯网(52im.net) & Jack Jiang.
// The RainbowChat Project. All rights reserved.
//
// 【本产品为著作权产品，合法授权后请放心使用，禁止外传！】
// 【本次授权给：<广西木子科技有限公司>，授权编号：<NT220402151538>，代码指纹：<A.648883738.885>，技术对接人微信：<ID: Lingmuziyi>】
// 【授权寄送：<收件：李先生、地址：南宁市科园西十路11号国电智能大厦1101F、电话：17736659550、邮箱：yingshashou@vip.qq.com>】
//
// 【本系列产品在国家版权局的著作权登记信息如下】：
// 1）国家版权局登记名(简称)和权证号：RainbowChat    （证书号：软著登字第1220494号、登记号：2016SR041877）
// 2）国家版权局登记名(简称)和权证号：RainbowChat-Web（证书号：软著登字第3743440号、登记号：2019SR0322683）
// 3）国家版权局登记名(简称)和权证号：RainbowAV      （证书号：软著登字第2262004号、登记号：2017SR676720）
// 4）国家版权局登记名(简称)和权证号：MobileIMSDK-Web（证书号：软著登字第2262073号、登记号：2017SR676789）
// 5）国家版权局登记名(简称)和权证号：MobileIMSDK    （证书号：软著登字第1220581号、登记号：2016SR041964）
// * 著作权所有人：江顺/苏州网际时代信息科技有限公司
//
// 【违法或违规使用投诉和举报方式】：
// 联系邮件：jack.jiang@52im.net
// 联系微信：hellojackjiang
// 联系QQ号：413980957
// 授权说明：http://www.52im.net/thread-1115-1-1.html
// 官方社区：http://www.52im.net
package com.x52im.rainbowchat;

import java.io.IOException;
import java.util.Base64;
import java.util.Map;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import com.alibaba.fastjson.JSON;
import com.eva.epc.common.util.CommonUtils;
import com.eva.epc.common.util.Platform;
import com.eva.framework.HttpController;
import com.eva.framework.Processor;
import com.eva.framework.Processor.User;
import com.eva.framework.dto.DataFromClient;
import com.eva.framework.dto.DataFromClient4Web;
import com.eva.framework.dto.DataFromServer;
import com.eva.framework.dto.LoginInfo2;
import com.eva.framework.dto.LogoutInfo;
import com.eva.framework.dto.SysProcessorConst;
import com.eva.framework.utils.EndsExceptionFactory;
import com.eva.framework.utils.LoggerFactory;
import com.google.gson.Gson;
import com.x52im.rainbowchat.common.dto.cnst.EncodeConf;
import com.x52im.rainbowchat.common.dto.cnst.MyProcessorConst;
import com.x52im.rainbowchat.common.dto.cnst.OperateLog;
import com.x52im.rainbowchat.http.logic.GroupChatProcessor;
import com.x52im.rainbowchat.http.logic.LogicProcessor2;
import com.x52im.rainbowchat.http.logic.LogicUtils;
import com.x52im.rainbowchat.http.logic.dto.RosterElementEntity;
import com.x52im.rainbowchat.http.util.JedisTools;

/**
 * RainbowChat的服务端HTTP REST接口服务的MVC核心控制器实现类
 * （专用于浏览器端通过HTTP GET方式的JSONP数据交换格式）.
 *
 * <p>
 * <b>说明：</b><br>
 * 本子类只需实现用户级的业务逻辑.
 *
 * <p>
 * <b>调用：</b>HTTP的GET方式（因为GET方式是JSONP的唯一支持调用方式，无法使用POST）。<br>
 * <b>用途：</b>本类主要用于实现Web浏览器端的http rest接口调用，服务端返回数据格式为JSONP（意味着本
 * 类提供的http rest接口可以在浏览器端彻底解决跨域调用问题）。
 *
 * <p>
 * <b>何为浏览器跨域问题？</b><br>
 * 为了安全考虑，浏览器厂商禁止网页端JS代码里调用当前域名、端口、子域名之外的链接（具体请自行百度）
 * ，这将导致您的浏览器端代码无法调用当前IM服务之外的任何链接。而JSONP数据交换格式是目前浏览器端
 * 用于解决跨域安全问题的最佳方案。有关浏览器跨域问题和JSONP的知识，请自行百度或查阅资料！
 *
 * @author Jack Jiang(http://www.52im.net/space-uid-1.html)
 * @since 4.5
 */
public class MyControllerJSONP extends HttpController {
    private final static long serialVersionUID = 1L;

    public final static String TAG = "HTTP.JSONP";

    /**
     * 是否显示debug信息
     */
    public static boolean DEBUG = true;

    public static Gson g = new Gson();

    private LogicProcessor2 logicProcessor = new LogicProcessor2();
    private GroupChatProcessor groupChatProcessor = new GroupChatProcessor();

    /**
     * <p>
     * 核心调度处理方法.<br>
     * <br>
     * 本方法默认会进行用户登陆认证处理:<br>
     * <ul>
     * <li>当参数{@code fc}中的DataFromClient.getProcessorId()== {@link SysProcessorConst#PROCESSSOR_LOGIN}
     * 时，进行用户登陆认证处理——首先进行版本检查 {@link #checkClientVersion(DataFromClient, HttpServletRequest, HttpServletResponse)}
     * ，接着再调用方法 {@link #login(DataFromClient, HttpServletRequest, HttpServletResponse)}验证登陆信息.</li>
     * <li>当参数{@code fc}中的 DataFromClient.getProcessorId()== {@link SysProcessorConst#PROCESSSOR_LOGOUT}
     * 时，进行用户登陆(会话)注销处理( {@link HttpSession#invalidate()} ).</li>
     * <li>否则如果用户已经是经过登陆认证的合法用户则直接进入方法 {@link #switchUserProcessor(DataFromClient, HttpServletRequest, HttpServletResponse, User)}
     * 进行其它业务请求调度处理，如果用户不是合法用户则直接拒绝处理请求并抛出异常. </li>
     * </ul>
     *
     * @param fc  封装了客户端发过来的相关数据（本方法中是必须的参数）
     * @param req 客户端请求实例引用（把它传过来完全是未了考虑以后可能有其它作用，本方法中它不是必须的）
     * @param res 客户端响应实例引用（把它传过来完全是未了考虑以后可能有其它作用，本方法中它不是必须的）
     * @return 处理完返回的对象（要给客户端的）
     * @throws Exception 本方法调用中出现任何异常时都会抛出，方法将中断执行
     * @see #switchUserProcessor(DataFromClient, HttpServletRequest, HttpServletResponse, User)
     */
    // 本方法是父类中的抽象方法，子类中必须实现，从而实现自已的真正MVC控制器业务逻辑
    @Override
    protected Object dispatch(DataFromClient dfc, HttpServletRequest req, HttpServletResponse res) throws Exception {
        Object retObj = null;
        int processor_id = dfc.getProcessorId();

        LoggerFactory.getLog().info("〖 [" + TAG + "][浏览器:" + dfc.getDevice() + "] 捕获REST【接口"
                + MyControllerJSON.printRestNum(processor_id, dfc.getJobDispatchId(), dfc.getActionId())
                + "】的请求，安全token=" + dfc.getToken() + " 〗");

        Processor processor = null;

        // Rest框架需要首先对客户端发送过来的数据进行类型检查，防止非String类型的
        // 数据的恶意传递（因为浏览器传过来的肯定都是String）
        if ((dfc.getNewData() != null && !(dfc.getNewData() instanceof String))
                || (dfc.getOldData() != null && !(dfc.getOldData() instanceof String))) {
            String ds = "[" + TAG + "]Rest框架要求客户端发过来的数据必须是JSON文本！而现在拿到的是：" +
                    "newData=" + (dfc.getNewData() != null ? dfc.getNewData().getClass().getName() : "")
                    + ", oldData=" + (dfc.getOldData() != null ? dfc.getOldData().getClass().getName() : "");
            LoggerFactory.getLog().error(ds);
            throw new RuntimeException(ds);
        }
        // 【接口1013】浏览器端登陆接口（Web浏览器端可使用）
        else if (processor_id == MyProcessorConst.PROCESSOR_LOGIN_4ALL) {
            LoginInfo2 ai = JSON.parseObject((String) dfc.getNewData(), LoginInfo2.class);

            String uidOrmail = ai.getLoginName();
            String oprContent = ai.getDeviceInfo();

            //** 登陆验证
            LoggerFactory.getLog().debug("[" + TAG + "]<登陆时：身份验证START> " + uidOrmail + "，接下来将进入真正的登陆验证流程.");
            // 登陆认证：返回null则表示认证实现，否则返回的是登陆用户的个人信息
            RosterElementEntity authedUser = MyControllerJSON.authrizationLogin(ai, req.getRemoteHost(), false);
            if (authedUser == null) {
                LoggerFactory.getLog().debug("[" + TAG + "]<登陆时：身份验证END> " + uidOrmail + "的账号无效或登陆密码不正确(在其它手机修改过？).");
                OperateLog oprLog = new OperateLog(OperateLog.OPERATE_LOG_TYPE_LOGIN_FALIURE, uidOrmail, oprContent);
                LogicUtils.addOperateLog(oprLog);
            } else {
                LoggerFactory.getLog().debug("[" + TAG + "]<登陆时：身份验证END> " + uidOrmail + "身份认证成功！");
                // 记录操作日志
                OperateLog oprLog = new OperateLog(OperateLog.OPERATE_LOG_TYPE_LOGIN_SUCCESS, uidOrmail, oprContent);
                LogicUtils.addOperateLog(oprLog);
            }

            //** 返回值
            return JSON.toJSONString(authedUser);
        }
        // 【接口-2】浏览器端注销登陆接口（Web浏览器端可使用）
        else if (processor_id == MyProcessorConst.PROCESSSOR_LOGOUT) {
            LogoutInfo ao = JSON.parseObject((String) dfc.getNewData(), LogoutInfo.class);
            MyControllerJSON.authrizationLogout(ao, req.getRemoteHost());
            return null;
        } else {
            // 出于安全考虑，此token可用于检查每一个http rest请求的合法性，检查策略可由业务逻辑层来考虑和处理，具体请开发者据自已的安全策略来定制
//            boolean isPass = checkToken(dfc);

            switch (processor_id) {
                // IM的基本业务逻辑接口处理器
                case MyProcessorConst.PROCESSOR_LOGIC:
                    processor = logicProcessor;
                    break;
                // 群组聊天相关管理接口的处理器
                case MyProcessorConst.PROCESSOR_GROUP_CHAT:
                    processor = groupChatProcessor;
                    break;
                //找不到处理器
                default:
                    throw EndsExceptionFactory.INVALID_PROCESSOR_ID(processor_id);
            }

            String basePath = req.getScheme() + "://" + req.getServerName() + ":" + req.getServerPort() + req.getContextPath() + "/";
//			System.out.println("[HTTP]basePath = " +  basePath);
            // 借用一个User对象，仅仅是用来存放Ip地址而已
            User u = new User();
            u.setIp(req.getRemoteHost());
            if (processor != null) {
                if (processor instanceof LogicProcessor2) {
                    retObj = processor.process(dfc.getJobDispatchId()
                            , dfc.getActionId()
                            , dfc.getNewData()
                            , new Object[]{basePath, dfc.getOldData()}
                            , u);
                } else {
                    retObj = processor.process(dfc.getJobDispatchId()
                            , dfc.getActionId(), dfc.getNewData()
                            , dfc.getOldData(), u);
                }

                // 返回值类型检查
                if (retObj != null && !(retObj instanceof String)) {
                    String ds = "[" + TAG + "]Rest框架中processor处理完成" +
                            "的返回值必须是JSONP文本哦！(retObj=" + retObj.getClass().getName() + ")";
                    LoggerFactory.getLog().error(ds);
                    throw new RuntimeException(ds);
                }
            }

            return retObj;
        }
    }

    /**
     * 核心处理方法.<br>
     * <p>
     * 本方法会确保处理完成后写回一个DataFromServer对象（客户端通过它可知是否处理成功等信息）给客端，如果处理成功则
     * DataFromServer对象中的sucess字值会被设置成true且returnValue会设置成处理完成的返回的对象，否则sucess将被设置成
     * false且returnValue里存放的将是错误消息文本（该文本不能保证一定不是null）.
     * </p>
     * <pre>
     * 	注：子类的处理实现推荐overload方法processImpl(DataFromClient, HttpServletRequest, HttpServletResponse)
     * ，一般情况下不推荐重写本方法.
     * </pre>
     *
     * @param req HttpServletRequest 客户端请求HttpServletRequest对象
     * @param res HttpServletResponse 客户端响应HttpServletResponse对象
     * @throws Exception 本方法调用中出现任何异常时都会抛出
     * @see #processImpl(DataFromClient, HttpServletRequest, HttpServletResponse)
     * @see DataFromServer
     */
    @Override
    // 重写本父类方法的目的，是实现向web浏览器端返回JSONP数据格式
    protected void process(HttpServletRequest req, HttpServletResponse res) throws Exception {
        //服务端是否要写回数据给客户端，此字段目前为保留字段，未有具体意义
        boolean needRecieve = true;

        String retFormat = DataFromClient4Web.RET_FORMAT_JSON;
        String retCallback = null;

        //处理完成后将要返回给客端的数据对象（通过它可知是否处理成功等信息）
        DataFromServer objFromServer = null;//new PojoFromServer();
        try {
            if (DEBUG) {
                LoggerFactory.getLog().debug("[" + TAG + "] - >>S 客户端Cookie=" + req.getRequestedSessionId()
                        + " ip=" + req.getRemoteHost() + ":" + req.getRemotePort());
            }

            //第1/3步：读取Web浏览器端发过来的数据
            DataFromClient4Web dataFromClient = recieveFromClient(req);
            retFormat = dataFromClient.getFormat();
            retCallback = dataFromClient.getJsoncallback();

//			//* 服务端是否要写回数据给客户端
//			needRecieve = dataFromClient.isDoInput();

            //第2/3步：提交处理器进行业务处理
            Object processedData = dispatch(dataFromClient, req, res);//processImpl(dataFromClient, req, res);
            if (objFromServer == null) {
                objFromServer = new DataFromServer();
            }
            //处理成功了
            objFromServer.setSuccess(true);
            objFromServer.setReturnValue(String.valueOf(processedData));
        } catch (Exception e) {
            objFromServer = new DataFromServer();
            objFromServer.setSuccess(false);
//			objFromServer.setReturnValue("在方法process(..)中出错,原因是:"+e.getMessage());
            // 2014-05-05日邮js改成只返回简单错误信息，而非完整Exception对象，目的是在跨平台实现时反射成JSON时不会StackOverflowError
            objFromServer.setReturnValue(CommonUtils.getExceptionStackTraceForPlainText(e));//e);
            LoggerFactory.getLog().warn("[" + TAG + "] - " + e.getMessage(), e);
            throw e;
        } finally {
            //* 服务端需要写回数据给客户端
            if (needRecieve) {
                //第3/3步：将处理完成的数据对象发回给客户端
                sendToClient(req, res, objFromServer, retFormat, retCallback);
            }

            if (DEBUG) {
                LoggerFactory.getLog().debug("[" + TAG + "] - >>E 处理完成,客户端" + " ip=" + req.getRemoteHost() + ":" + req.getRemotePort()
                        + " 的请处理成功了?" + objFromServer.isSuccess() + (needRecieve ?
                        " - ret=" + objFromServer.getReturnValue().getClass()//objFromServer.getReturnValue() 直接输出返回值用于早期MVC框架调试时，生产环境下将大幅影响系统性能
                        : " - [doInput=false,无需返回值给客户端]"));
            }
        }
    }

    /**
     * 将数据对象发回给Web浏览器端.
     *
     * @param res           客户端响应HttpServletResponse对象
     * @param objFromServer 要发送的可系列化的数据对象
     * @throws Exception 如果在操作OutputStream时出现异常
     */
    protected void sendToClient(HttpServletRequest req, HttpServletResponse res
            , DataFromServer objFromServer, String retFormat, String retCallBack) throws IOException {
        res.setContentType("text/html;charset=" + EncodeConf.ENCODE_TO_CLIENT);

        // 返回结果
        String retStr = null;
        try {
            // 如果返回结果是JSONP
            if (DataFromClient4Web.RET_FORMAT_JSONP.equals(retFormat)) {
                retStr = retCallBack + "(" + g.toJson(objFromServer) + ");";
            }
            // 否则返回的是JSON
            else {
                retStr = g.toJson(objFromServer);
            }

            res.getOutputStream().write(retStr.getBytes(EncodeConf.ENCODE_TO_CLIENT));

            if (DEBUG) {
                LoggerFactory.getLog().info("[" + TAG + "]【<== 处理完成-返回JSONP结果】:" + retStr);
            }
        } catch (Exception e2) {
            LoggerFactory.getLog().warn("[" + TAG + "] - " + e2.getMessage(), e2);
        }

//		if(DEBUG)
//		{
//			LoggerFactory.getLog().debug("["+TAG+"] - >>已处理完成客户端ip="+req.getRemoteHost()
//					+":"+req.getRemotePort()+"的GET请求，结果="+retStr);
//		}
    }

    /**
     * 读取PC客户端传过来的数据对象。
     *
     * @param req 客户端请求HttpServletRequest对象
     * @return 返回读取出来的客户端数据对象
     * @throws IOException            如果req.getInputStream()出现异常时抛出
     * @throws ClassNotFoundException 如果is.readObject()出现异常时抛出
     */
    @Override
    // 重写本父类方法的目的是读取Web浏览器端通过GET方式发过来的数据（因为RainbowChat-Web
    // 的浏览器端为了解决跨域问题，只能使用JONP的方式，而JSONP又只能用http GET而不能http POST）
    protected DataFromClient4Web recieveFromClient(HttpServletRequest req)
            throws IOException, ClassNotFoundException {
        int processorId = CommonUtils.getIntValue(req.getParameter("processorId"), -9999999);
        int jobDispatchId = CommonUtils.getIntValue(req.getParameter("jobDispatchId"), -9999999);
        int actionId = CommonUtils.getIntValue(req.getParameter("actionId"), -9999999);
        String token = req.getParameter("token");

        String newData = MyControllerJSON.SQLSave(getStringFromURL(req.getParameter("newData")));
        String oldData = MyControllerJSON.SQLSave(getStringFromURL(req.getParameter("oldData")));

        String retFormat = req.getParameter("format");
        // 本参数只在format=jsonp时有意义，表示jsonp对应的客户端JS回调函数
        // 补充：为了兼容jQuery的跨域回调，参数名就定为jsoncallback了
        String retCallBack = req.getParameter("jsoncallback");

        if (DEBUG) {
            LoggerFactory.getLog().info("[" + TAG + "]【==> 收到接口请求-数据原型】: processor_id=" + processorId
                    + ", job_id=" + jobDispatchId + ", action_id=" + actionId + ", token=" + token
                    + ", new_data=" + newData + ", old_data=" + oldData
                    + ", format=" + retFormat + ", jsoncallback=" + retCallBack);
        }

        DataFromClient4Web dfc = DataFromClient4Web.n();
        dfc.setProcessorId(processorId);
        dfc.setJobDispatchId(jobDispatchId);
        dfc.setActionId(actionId);
        dfc.setToken(token);
        dfc.setNewData(newData);
        dfc.setOldData(oldData);
        dfc.setDevice(2);
        dfc.setFormat(retFormat);
        dfc.setJsoncallback(retCallBack);

        return dfc;
    }

    /**
     * 解决http GET方式发过来的参数中的中文乱码问题。
     * 参考文章：https://blog.csdn.net/chenssy/article/details/42963595
     *
     * @param strFromURL
     * @return
     */
    private static String getStringFromURL(String strFromURL) {
        try {
            if (strFromURL != null) {
                // 注意：windows环境下（无论是开发，还是生产部署）运行时，请勿设
                //      置tomcat中server.xml的“URIEncoding="UTF-8"”，否则请去掉本行针对windows的特殊处理，
                //      详见文章：https://www.jb51.net/article/80181.htm
//				if(Platform.isWindows())
//					return new String(strFromURL.getBytes("ISO-8859-1"),"UTF-8");
//				// Liux等类Unix操作系统下，解决乱码，请在tomcat中server.xml的Connector中增加“URIEncoding="UTF-8"”配置属性即可
//				else
                return strFromURL;
            }
        } catch (Exception e) {
            LoggerFactory.getLog().error(e.getMessage(), e);
        }
        return strFromURL;
    }

    /**
     * 强制保护浏览器端JSON.parese()可能存在多次JSON嵌套时，因特殊字符未被尽职转义而解析出错的问题。
     *
     * @param originalStr
     * @return
     */
    public static String JSONSave(String originalStr) {
        if (originalStr != null) {
            // 浏览器端的JSON.parese()函数对多层嵌套JSON中有些特殊字符没有办法正确解析，
            // 因而以下代码为了保证浏览器端的正确性，强制进行个别特殊字符的转义 - 启用于20190412日
            return originalStr
//					.replaceAll("\"", "&quot;")
//					.replaceAll("\'", "&#39;")
//					.replaceAll("\\", "\\\\")
                    .replaceAll("\n", " ")//\\n
                    .replaceAll("\r", " ")//\\r
                    .replaceAll("\t", " ")//\\t
//					.replaceAll("\\n", " ")
//					.replaceAll("\\r", " ")
//					.replaceAll("\\t", " ")
                    ;
        }

        return originalStr;
    }
}
